Kotlin Coroutines 是 Kotlin 提供的一個支援協程的 Library ,讓開發者有另一種方式處理非同步問題。
關於協程的概念我不再贅述,網路上或是官網都有對此概念有更詳盡的說明,未來有機會可能會在挑戰後單獨開一篇分享,因為事實上我對於 Coroutines 本質的了解現在也還沒到可以分享的地步。讓我在這次挑戰使用 Coroutines 是因為我曾經在某篇文章看過一個結論:
有了協程,你的異步程式看起來就像同步程式一樣
試想一個用戶登入的情境:
取得一個 token 後,再用這個 token 請求登入 API ,登入成功後再獲得一些用戶基本資料,接著顯示登入成功。
class ViewModel {
fun getToken(): Token {
// 做一些 API 耗時操作
return token 請求 access token
}
fun verify(token, userName): Boolean {
// 做一些 API 耗時操作
return result // 登入成功或失敗
}
fun requestAndSaveUserData(userName): {
// 做一些 API 耗時操作
return Success // 取得一些登入者的資料
}
......
fun login(userName: String) {
val token = getToken()
val result = verify(token, userName)
if (result) {
val success = requestAndSaveUserData(userName)
if (success) {
showLoginSuccess("Success")
}
}
}
}
所有函數都是耗時操作,因此不能直接在 UI 線程執行,同時每一個方法都依賴前一個方法的結果,三者都不能同時進行,那要怎麼辦呢?
第一種作法是使用 Callback ,在 Kotlin 中也可以使用 Lamnda Function :
fun getTokenAsync(action: (Token) -> Unit) {
......
}
fun verifyAsync(
token: Token,
userName: String,
action: (Boolean) -> Unit
) {
......
}
fun requestAndSaveUserDataAsync(
userName: String,
result: (Result) -> Unit
) {
......
}
......
fun login(userName: String) {
getTokenAsync { token ->
verifyAsync(token, userName) { verifyResult ->
requestAndSaveUserDataAsync(userName) { result ->
showLoginSuccess("Success")
}
}
}
}
...看起來很不舒服,對吧?
在一些簡單的情境使用 Callback 可以很好的處理問題,但是面對比較複雜的狀況時就很容易發生 "Call Hell" 了,而且也很難處理異常狀況。
接下來看看另一種十分熱門的做法,RxJava 的鏈式調用:
fun getToken(): Observable<Token> { ...... }
fun verify(userName): Observable<Boolean> {
return getToken()
.flatMap { token ->
......
}
}
fun requestAndSaveUserData(userName: String): Observable<Result> {
return verify(userName)
.flatMap { ...... }
}
fun login(userName: String) {
requestAndSaveUser(userName)
.subscribeOn(Schedulers.io())
.observerOn(AndroidSchedulers.mainThread())
.subscribe({
showLoginSuccess(it)
}, {
it.printStackTrace()
})
}
RxJava 豐富的操作符、簡單的線程調度及異常處理,讓大家都很滿意,但前提是你要對他有深入的了解!
許多教學文常常提到 RxJava 讓程式碼變得有條理,但那都是建立在了解 RxJava 及其複雜的操作符之上。昂貴的學習成本讓許多開發著卻步,那是否有更簡單的作法呢?
suspend fun getToken(): Token { ...... }
suspend fun verify(token, userName: String): Boolean { ...... }
suspend fun requestAndSaveUserData(userName: String): Result { ...... }
fun login(userName: String) {
GlobalScope.launch {
try {
val token = getToken()
val isLogin = verify(token, userName)
if (isLogin) {
val success = requestAndSaveUserData(userName)
if (success) {
showLoginSuccess("Success")
}
}
......
}
}
}
是不是簡單很多呢?
Coroutines 讓程式碼變得更加簡潔,同時異常處理也簡單很多,閱讀起來也十分的有條理,看起來跟同步執行幾乎沒有兩樣。
對於 Coroutines,這邊附上我的理解:
Coroutines 允許函式可以被 "掛起" (suspend),避免主線程的佔用,而且被掛起的函式後續也可以被恢復,並從暫停時保留的狀態繼續執行。
再白話一點的說法,以剛剛的例子說明:
在 Main thread 執行到一個耗時的 getToken() 時,由於需要等待 IO thread 的結果,所以先暫停 getToken() ,讓出 Main thread 去做其他的事,等到 IO thread 執行完畢後,再重新繼續 getToken() ,而這種控制 function 的 "暫停" -> "繼續" 的狀態切換,就是 Coroutines 實際上在做的事。
事實上 Coroutines 的概念早在 1960 年代就有了,而且早期的語言如 Lua、Simula ,一直到當今的 C#、python、Golang、Swift 等皆相繼引入 Coroutines 的概念,在別的領域裡十分流行,Android 則直到 Kotlin 成為官方語言後才以 Support Library 的方式引入,並在近年流行起來。
前面已經提到了,Kotlin 的 Coroutines 是以 Support Library 的方式提供給開發者使用:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1'
}
另外 Kotlin 還提供了 kotlinx-coroutines-android 給 Android 開發者使用,主要多增加了 Dispatchers.Main
讓開發者可以更輕易的切換到 Main thread ,同時也確保在 Android app 崩潰前有確實的紀錄 Log:
// Android 開發者可以只 import 此 library
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1'
}
這樣就完成 Coroutines 的環境配置,明天再來簡單的練習。